package org.unidata.mdm.data.service.segments;

import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.calculables.CalculableHolder;
import org.unidata.mdm.core.type.data.DataShift;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.formless.BundlesArray;
import org.unidata.mdm.core.type.formless.DataBundle;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.context.GetRelationTimelineRequestContext;
import org.unidata.mdm.data.service.impl.CommonRelationsComponent;
import org.unidata.mdm.data.type.calculables.impl.RelationRecordHolder;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.data.OriginRelationInfoSection;
import org.unidata.mdm.data.type.data.RelationType;
import org.unidata.mdm.data.type.data.impl.OriginRelationImpl;
import org.unidata.mdm.data.type.draft.DataDraftConstants;
import org.unidata.mdm.data.type.draft.DataDraftOperation;
import org.unidata.mdm.data.type.keys.RecordEtalonKey;
import org.unidata.mdm.data.type.keys.RecordOriginKey;
import org.unidata.mdm.data.type.keys.RelationEtalonKey;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.data.type.keys.RelationOriginKey;
import org.unidata.mdm.data.type.timeline.RelationTimeline;
import org.unidata.mdm.data.util.StorageUtils;
import org.unidata.mdm.draft.type.Draft;
import org.unidata.mdm.draft.type.Edition;
import org.unidata.mdm.meta.configuration.Descriptors;

/**
 * @author Mikhail Mikhailov on Sep 26, 2020
 * Draft timeline and keys generation support.
 */
public interface RelationDraftTimelineSupport {
    /**
     * Gets common component, needed for key fetch.
     * @return component
     */
    CommonRelationsComponent getCommonRelationsComponent();
    /**
     * Gets meta model service.
     * @return the service
     */
    MetaModelService getMetaModelService();
    /**
     * Generates timeline from record and edition draft.
     * @param draft the draft
     * @param edition the edition
     * @return timeline
     */
    default Timeline<OriginRelation> timeline(Draft draft, @Nullable Edition edition) {

        RelationKeys keys = keys(draft, edition);

        List<CalculableHolder<OriginRelation>> calculables = Collections.emptyList();
        if (Objects.nonNull(edition) && Objects.nonNull(edition.getContent())) {

            BundlesArray bundles = edition.getContent();
            calculables = bundles.stream()
                .map(b -> origin(b, edition, keys))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        }

        return new RelationTimeline(keys, calculables);
    }
    /**
     * Generates or loads the keys, adjusting state according to current operation.
     * @param draft the draft
     * @return keys
     */
    default RelationKeys keys(Draft draft, Edition edition) {
        boolean isNew = BooleanUtils.isTrue(draft.getVariables().valueGet(DataDraftConstants.IS_NEW_RECORD));
        return isNew ? generate(draft, edition) : load(draft);
    }
    /**
     * Generates a calculable holder from a bundle.
     * @param bundle the bundle
     * @param edition the edition
     * @param ok the keys
     * @return holder
     */
    default CalculableHolder<OriginRelation> origin(DataBundle bundle, Edition edition, RelationKeys keys) {

        if (Objects.isNull(bundle)) {
            return null;
        }

        String boxKey = bundle.getVariables().valueGet(DataDraftConstants.BOX_KEY);
        int revision = bundle.getVariables().valueGet(DataDraftConstants.REVISION);
        Instant from = bundle.getVariables().valueGet(DataDraftConstants.VALID_FROM);
        Instant to = bundle.getVariables().valueGet(DataDraftConstants.VALID_TO);
        Instant ts = bundle.getVariables().valueGet(DataDraftConstants.UPDATE_TIMESTAMP);
        OperationType type = bundle.getVariables().valueGet(DataDraftConstants.OPERATION_TYPE, OperationType.class);
        RecordStatus status = bundle.getVariables().valueGet(DataDraftConstants.PERIOD_STATUS, RecordStatus.class);

        Date timestamp = new Date(ts.toEpochMilli());

        OriginRelationImpl origin = new OriginRelationImpl()
            .withDataRecord(bundle.getRecord())
            .withInfoSection(new OriginRelationInfoSection()
                    .withRelationName(keys.getRelationName())
                    .withRelationType(keys.getRelationType())
                    .withValidFrom(from == null ? null : new Date(from.toEpochMilli()))
                    .withValidTo(to == null ? null : new Date(to.toEpochMilli()))
                    .withFromEntityName(keys.getFromEntityName())
                    .withToEntityName(keys.getToEntityName())
                    .withStatus(status)
                    .withShift(DataShift.PRISTINE)
                    .withOperationType(type)
                    .withRelationOriginKey(keys.findByBoxKey(boxKey))
                    .withCreateDate(timestamp)
                    .withUpdateDate(timestamp)
                    .withCreatedBy(edition.getCreatedBy())
                    .withUpdatedBy(edition.getCreatedBy())
                    .withRevision(revision)
                    .withMajor(0)
                    .withMinor(0));

        return new RelationRecordHolder(origin);
    }
    /**
     * Loads exisitng record's keys using draft object.
     * @param draft the draft object
     * @return keys
     */
    default RelationKeys load(Draft draft) {

        String etalonId = draft.getVariables().valueGet(DataDraftConstants.ETALON_ID);
        RelationKeys k = getCommonRelationsComponent().identify(GetRelationTimelineRequestContext.builder()
                .relationEtalonKey(etalonId)
                .build());

        // If the draft has no editions saved yet, just return the keys
        // to enable checks against original relation state.
        if (!draft.hasEditions()) {
            return k;
        }

        DataDraftOperation operation = draft
                .getVariables()
                .valueGet(DataDraftConstants.OPERATION_CODE, DataDraftOperation.class);

        switch (operation) {
        case UPSERT_DATA:
        case RESTORE_RECORD:
        case RESTORE_PERIOD:
        case DELETE_PERIOD:

            if (k.getEtalonKey().getStatus() == RecordStatus.ACTIVE) {
                return k;
            }

            return RelationKeys.builder(k)
                    .etalonKey(RelationEtalonKey.builder(k.getEtalonKey())
                            .status(RecordStatus.ACTIVE)
                            .build())
                    .build();
        case DELETE_RECORD:

            if (k.getEtalonKey().getStatus() == RecordStatus.INACTIVE) {
                return k;
            }

            return RelationKeys.builder(k)
                    .etalonKey(RelationEtalonKey.builder(k.getEtalonKey())
                            .status(RecordStatus.INACTIVE)
                            .build())
                    .build();
        default:
            break;
        }

        return k;
    }
    /**
     * Generates phantom keys for a new record using draft object.
     * @param draft the draft object
     * @param edition the edition
     * @return keys
     */
    default RelationKeys generate(Draft draft, @Nullable Edition edition) {

        Date createDate = Objects.nonNull(edition) ? edition.getCreateDate() : new Date();
        String createdBy = Objects.nonNull(edition) ? edition.getCreatedBy() : SecurityUtils.getCurrentUserName();

        // Calculate status even if this is a new record to cover the cases
        // when a new record is "deleted" without publishing.
        DataDraftOperation operation = draft
                .getVariables()
                .valueGet(DataDraftConstants.OPERATION_CODE, DataDraftOperation.class);

        RecordStatus status = RecordStatus.ACTIVE;
        if (operation == DataDraftOperation.DELETE_RECORD) {
            status = RecordStatus.INACTIVE;
        }

        String etalonId = draft.getVariables().valueGet(DataDraftConstants.ETALON_ID);
        String relationName = draft.getVariables().valueGet(DataDraftConstants.RELATION_NAME);
        RelationElement re = getMetaModelService().instance(Descriptors.DATA).getRelation(relationName);

        RelationOriginKey rok = RelationOriginKey.builder()
                .id(StringUtils.EMPTY)
                .initialOwner(UUID.fromString(etalonId))
                .sourceSystem(draft.getVariables().valueGet(DataDraftConstants.SOURCE_SYSTEM))
                .enrichment(false)
                .revision(0)
                .status(RecordStatus.ACTIVE)
                .from(RecordOriginKey.builder()
                        .createDate(draft.getCreateDate())
                        .createdBy(draft.getCreatedBy())
                        .updateDate(createDate)
                        .updatedBy(createdBy)
                        .enrichment(false)
                        .entityName(re.getLeft().getName())
                        .externalId(draft.getVariables().valueGet(DataDraftConstants.FROM_EXTERNAL_ID))
                        .sourceSystem(draft.getVariables().valueGet(DataDraftConstants.FROM_SOURCE_SYSTEM))
                        .id(StringUtils.EMPTY)
                        .status(RecordStatus.ACTIVE)
                        .initialOwner(UUID.fromString(draft.getVariables().valueGet(DataDraftConstants.FROM_ETALON_ID)))
                        .revision(0)
                        .build())
                .to(RecordOriginKey.builder()
                        .createDate(draft.getCreateDate())
                        .createdBy(draft.getCreatedBy())
                        .updateDate(createDate)
                        .updatedBy(createdBy)
                        .enrichment(false)
                        .entityName(re.getRight().getName())
                        .externalId(draft.getVariables().valueGet(DataDraftConstants.TO_EXTERNAL_ID))
                        .sourceSystem(draft.getVariables().valueGet(DataDraftConstants.TO_SOURCE_SYSTEM))
                        .id(StringUtils.EMPTY)
                        .status(RecordStatus.ACTIVE)
                        .initialOwner(UUID.fromString(draft.getVariables().valueGet(DataDraftConstants.TO_ETALON_ID)))
                        .revision(0)
                        .build())
                .createDate(draft.getCreateDate())
                .createdBy(draft.getCreatedBy())
                .updateDate(createDate)
                .updatedBy(createdBy)
                .build();

        int shard = draft.getVariables().valueGet(DataDraftConstants.SHARD);
        long lsn = draft.getVariables().valueGet(DataDraftConstants.LSN);

        return RelationKeys.builder()
            .etalonKey(RelationEtalonKey.builder()
                    .id(etalonId)
                    .lsn(lsn)
                    .status(status)
                    .from(RecordEtalonKey.builder()
                            .id(draft.getVariables().valueGet(DataDraftConstants.FROM_ETALON_ID))
                            .lsn(draft.getVariables().valueGet(DataDraftConstants.FROM_LSN))
                            .status(RecordStatus.ACTIVE)
                            .build())
                    .to(RecordEtalonKey.builder()
                            .id(draft.getVariables().valueGet(DataDraftConstants.TO_ETALON_ID))
                            .lsn(draft.getVariables().valueGet(DataDraftConstants.TO_LSN))
                            .status(RecordStatus.ACTIVE)
                            .build())
                    .build())
            .originKey(rok)
            .supplementaryKeys(Collections.singletonList(rok))
            .published(false)
            .shard(shard)
            .node(StorageUtils.node(shard))
            .fromEntityName(re.getLeft().getName())
            .toEntityName(re.getRight().getName())
            .relationName(re.getName())
            .relationType(RelationType.fromModel(re))
            .createDate(draft.getCreateDate())
            .createdBy(draft.getCreatedBy())
            .updateDate(createDate)
            .updatedBy(createdBy)
            .build();
    }
}
